iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 26
4
Modern Web

在 React 生態圈內打滾的一年 feat. TypeScript系列 第 26

Day25 | 善用 interface 抽象概念,為 Class 找到出路

  • 分享至 

  • xImage
  •  

前言

如果沒有碰過其他物件導向語言的話,Interface 應該會顯得很陌生,筆者剛學的時候也不例外,但其實它使用起來真的是很強大,本篇會介紹 Interface 的基本用法,在下個篇章會另外用一些設計模式,讓大家體會 Class 與 Interface 的實際運用。


前置準備

  1. 文中的專案會以 Day24 的專案架構繼續講解,如果未跟到前一天的進度,可以從 GitHub 上 Clone 下來。
  2. 一顆擁有學習熱忱的心。

Interface 接口

Interface 被稱作介面或是接口,它主要在

與 Class 約定行為,但 Interface 只描述有哪些 Method 和 Property ,不包含怎麼執行。

在 Interface 中,會描述自身該有哪些 Method 或 Property ,但 Interface 不會也不能實作這些功能,僅僅是要求使用該 Interface 的 Class 得實現它,否則就會發生錯誤,無法正確編譯。

使用方法

建立 Interface

在宣告一個 Interface 時,直接使用 interface 做關鍵字宣告,這裡筆者習慣將 Interface 的命名前面都加上一個大寫的 I,避免和一般的 Class 搞混,但這是 C# 出身的習慣,如果是其他程式語言的 Interface 應該不會加上前綴 I,這裡大家可以斟酌看看。

首先在專案的目錄下建立新檔案 ICar.ts,並在檔案裡宣告一個基本的 Interface,並將它做 export

interface ICar {
  model: string;
  move:() => void;
};

export default ICar;

Icar 中擁有兩個 Property,一個是 model 另一個是 Method move,這代表如果有 Class 要實作這個接口,就必須將 modelmove 都實作出來,否則就會錯誤,但要怎麼讓 Class 實作 Interface 呢?讓我們打開 src/Car.ts,其中有個 Class 叫做 Car

如果 Car 要實作 ICar,只需要在 Class 的名稱後使用 implements,並加上 Interface 的名稱:

import ICar from './ICar';

class Car implements ICar {
  protected model: string = 'GQSM-X';
  public color: string;

  constructor(color: string) {
    this.color = color;
  }

  public getDescription(): string {
    return `型號是:${this.model}(${this.color})`;
  }
}

加上後會發現原本正確的 Car 出現紅線警告:

這個錯誤的意思是 Car 既然實作了 ICar,那就要好好遵守這個 Instance 需要的實現,然後下方會提示說有遺漏哪些 Property,在這個例子裡,被遺漏的 Property 是 move,為了移除此錯誤,必須在 Car 裡實作 move

class Car implements ICar {
  /* 其餘省略 */
  public move(): string {
    return '出發前進!';
  }
}

加上 move 後,Car 下方的紅線並沒有消失,那是因為 Interface 所定義的 Property 都一定要是 Public,而在 Car 之中的 model 目前是 Protected 狀態,因此警告還是存在,並要求將 model 變更為 Public:

這個問題只要將 modelprotected 改成 public 後就沒問題了,當然!如果 Class 沒有正確實現 Interface,TypeScript 的編譯也不會通過的。

另外,Car 不只實現了 Interface 內的行為,也有其他 Interface 內沒有描述到的 Method 和 Property,這是沒問題的,因為

Interface 只是為 Class 約束了最低需要哪些行為而已。

而且,一個 Class 也能夠同時被多個 Interface 給約束,每個 Interface 間使用逗號間隔:

class Car implements ICarA, ICarB {
  /**...**/
}

簡單運用

本節會簡單說明一下 Class 為何需要實現 Interface,以及基本的運用,下一章會再用設計模式講解一次。

假設今天有個 Method,我們將它放到 index.ts 中,它會接收 Car 來執行 move 這個動作,先到 Car.ts 中將 Car 做 export:

class Car implements ICar {
  /* 其餘省略 */
} 

export default Car;

打開 index.ts,將 Carimport,並宣告一個執行 move 的方法 startMove

import Car from './Car';

const startMove = (car: Car): void => {
  console.log(`開始${car.move()}`);
}

startMove(redCar); // 開始出發前進!

可以看見 startMove 接收了一個型別為 Car 的參數,代表我們可以將使用 Car 建立的 Instance 傳入執行。

但是會 move 的交通工具不只有車而已,除了車以外還有還有飛機 Airplane 讓我們替它建立一個新檔案 Airplane.ts,並撰寫 Class 做 export

class Airplane {
  public move(): string {
    return '起飛出發!'
  }
}

export default Airplane

如果一樣要讓 Airplane 執行 startMove 該怎麼做呢?再創建一個 airplaneStartMove 然後傳入 Airplane 型別的參數嗎?像這樣子:

const redCar = new Car('Red');
const airplane = new Airplane();

const startMove = (car: Car): void => {
  console.log(`開始${car.move()}`);
}

const airplaneStartMove = (airplane: Airplane): void => {
  console.log(`開始${airplane.move()}`);
}

startMove(redCar); // 開始出發前進!
airplaneStartMove(airplane); // 開始起飛出發!

明明是同樣都是 move 的行為,卻要寫兩個內容一模一樣,只是傳入參數型別不同的 Method 出來,而且除了車、飛機外,還可能會無限擴充出各種能夠移動的 Class,如果要為所有的 Class 都專寫一個 Method,這種程式太令人哭泣了。

Interface 就是為了這種情況出現了,它可以讓 Method 依賴 Interface 而不是 Class,例如說,今天我將 move 這個 Method 從 ICar 提出,並建立另一個檔案 IMove,創建關於 move 這個行為的 IMove

interface IMove {
  move(): void;
}

export default IMove;

接著到 CarAirplane 的 Class,替它們加上 IMove

Car.ts

import IMove from './IMove';

class Car implements ICar, IMove {
  /* 其餘省略 */
}

Airplane.ts

import IMove from './IMove';

class Airplane implements IMove {
  /* 其餘省略 */
}

這時候因為 CarAirplane 都實現了 IMove,所以他們之中一定會有 move 這個 Method,所以在執行 move 時,就能夠不必對 Class,而是統一用 Interface 中執行就好,下方重新修改 startMove

import IMove from './IMove';

const startMove = (transportation: IMove): void => {
  console.log(`開始${transportation.move()}`);
}

可以看見原本的參數型別從 Class 變成 Interface,如此一來,只要是實作該 Interface 的 Class 都可以傳入 startMove 裡執行:

const redCar = new Car('Red');
const airplane = new Airplane();

const startMove = (transportation: IMove): void => {
  console.log(`開始${transportation.move()}`);
}

startMove(redCar); // 開始出發前進!
startMove(airplane); // 開始飛行前進!

到目前為止,昨天和今天兩篇文章就建立了兩個 Class 和兩個 Interface,如果全都放在根目錄中,會變得難以管理,因此在根目錄裡創建兩個資料夾,分別就是 class 與 interface,然後將 Class 全都丟到 class 目錄中,Interface 就放到 interface 裡管理:

|-class
 |-Airplane.ts
 |-Car.ts
|-interface
 |-ICar.ts
 |-IMove.ts
|-index.ts
|-index.js

別忘了移動完後,也要把程式中 import 的路徑修正,確認沒問題就可以執行 tsc index.ts 做編譯,如果成功編譯就能繼續輸入 node index.js 指令執行:

本文的範例程式碼會提供在 GitHub 上,歡迎各位參考:)


結尾

本篇簡單的介紹了 Interface 的基本用法以及使用情境,筆者認為

只要適當的使用 Interface,就能降低程式間的耦合性。

以文中的例子而言,如果所有的 Method 都以特定 Class 的實體作為參數傳入,那 Class 對於該 Method 的耦合度就太高了,因為這麼做等於綁定了只能透過某個 Class 才能執行的情境,但如果是以 Interface 定義的話,不論是哪個 Class ,只要它實作了指定的 Interface 就可以執行該 Method。

所以 Interface 也是一個抽象的概念,它只定義了「移動」這個行為,實現它的 Class 一定會移動,但是「怎麼移動」就不管了。

下一節會再繼續周旋在 Class 與 Interface 之間,以實作設計模式為主,希望能夠讓大家知道使用他們開發會多有趣靈活。

如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!


上一篇
Day24 | 只要別搞混 Class,你想得到通通有
下一篇
Day26 | 精選設計模式實戰,打通 interface 及 class 的運用觀念
系列文
在 React 生態圈內打滾的一年 feat. TypeScript31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言